﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using ProLinq.Projection;

namespace ProLinq.Mapping
{
	public class Mapper
	{
		#region Fields

		private Dictionary<MappingKey, MappingConfiguration> _cfgCache = new Dictionary<MappingKey, MappingConfiguration>(new MappingKeyComparer());
		
		#endregion

		#region Methods

		public void CreateMap<TSource, TResult>()
		{
			EnsureMap<TSource, TResult>();
		}

		/// <summary>
		/// Maps one object to another
		/// </summary>
		/// <typeparam name="TSource">Source type</typeparam>
		/// <typeparam name="TResult">Result type</typeparam>
		/// <param name="source">Source object</param>
		/// <param name="result">Result object (will be modified if TResult is a reference type)</param>
		/// <returns>Mapped object of TResult type (will be the same reference as result if TResult is a reference type)</returns>
		public TResult Map<TSource, TResult>(TSource source, TResult result)
		{
			if (source == null)
			{
				return result;
			}
			
			var cfg = EnsureMap<TSource, TResult>();
			return cfg.Map(source, result);
		}

		public TResult Map<TSource, TResult>(TSource source)
		{
			if (source == null)
			{
				return default(TResult);
			}

			var cfg = EnsureMap<TSource, TResult>();

			if (!typeof(TSource).IsValueType)
			{
				Object existingResult = null;
				if (cfg.ObjectCache.TryGetValue(source, out existingResult))
				{
					return (TResult)existingResult;
				}
			}

			var result = cfg.New(source);

			if (!typeof (TSource).IsValueType)
			{
				cfg.ObjectCache.Add(source, result);
			}

			return cfg.Map(source, result);
		}


		private MappingConfiguration<TSource, TResult> GetMap<TSource, TResult>()
		{
			var key = new MappingKey(typeof(TSource), typeof(TResult));

			MappingConfiguration cfg;
			if (_cfgCache.TryGetValue(key, out cfg))
			{
				return (MappingConfiguration<TSource, TResult>) cfg;
			}
			return null;
		}

		private MappingConfiguration<TSource, TResult> EnsureMap<TSource, TResult>()
		{
			var key = new MappingKey(typeof(TSource), typeof(TResult));
			MappingConfiguration cfg;
			if (_cfgCache.TryGetValue(key, out cfg))
			{
				cfg.Compile();
				return (MappingConfiguration<TSource, TResult>)cfg;
			}

			var cfgNew = CreateMapInternal<TSource, TResult>();
			_cfgCache.Add(key, cfgNew);
			return cfgNew;
		}


		private MappingConfiguration<TSource, TResult> CreateMapInternal<TSource, TResult>()
		{
			var cfg = new MappingConfiguration<TSource, TResult>(this);
			cfg.Compile();
			return cfg;
		}
		#endregion
	}
}
